---
id: logging
title: 18. 日志记录
sidebar_label: 18. 日志记录
---

## 18.1 关于日志

通常日志指的是**系统日志**和**程序日志**。

**系统日志** 是记录系统中硬件、软件和系统问题的信息，同时还可以监视系统中发生的事件。用户可以通过它来检查错误发生的原因，或者寻找受到攻击时攻击者留下的痕迹。系统日志包括系统日志、应用程序日志和安全日志。

**程序日志** 是程序运行中产生的日志，通常由框架运行时或开发者提供的日志。包括请求日志，异常日志、审计日志、行为日志等。

## 18.2 日志作用

在项目开发中，都不可避免的使用到日志。没有日志虽然不会影响项目的正确运行，但是没有日志的项目可以说是不完整的。日志在调试，错误或者异常定位，数据分析中的作用是不言而喻的。

- 调试

在项目调试时，查看栈信息可以方便地知道当前程序的运行状态，输出的日志便于记录程序在之前的运行结果。

- 错误定位

不要以为项目能正确跑起来就可以高枕无忧，项目在运行一段时候后，可能由于数据问题，网络问题，内存问题等出现异常。这时日志可以帮助开发或者运维人员快速定位错误位置，提出解决方案。

- 数据分析

大数据的兴起，使得大量的日志分析成为可能，ELK 也让日志分析门槛降低了很多。日志中蕴含了大量的用户数据，包括点击行为，兴趣偏好等，用户画像对于公司下一步的战略方向有一定指引作用。

## 18.3 日志级别

日志级别可以有效的对日志信息进行归类，方便准确的查看特定日志内容。通常日志类别有以下级别：

|        级别         | 值  |      方法      | 描述                                                                                                       |
| :-----------------: | --- | :------------: | ---------------------------------------------------------------------------------------------------------- |
|    Trace（跟踪）    | 0   |    LogTrace    | 包含最详细的消息。 这些消息可能包含敏感的应用数据。 这些消息默认情况下处于禁用状态，并且不应在生产中启用。 |
|    Debug（调试）    | 1   |    LogDebug    | 用于调试和开发。 由于量大，请在生产中小心使用。                                                            |
| Information（信息） | 2   | LogInformation | 跟踪应用的常规流。 可能具有长期值。                                                                        |
|   Warning（警告）   | 3   |   LogWarning   | 对于异常事件或意外事件。 通常包括不会导致应用失败的错误或情况。                                            |
|    Error（错误）    | 4   |    LogError    | 表示无法处理的错误和异常。 这些消息表示当前操作或请求失败，而不是整个应用失败。                            |
|  Critical（严重）   | 5   |  LogCritical   | 需要立即关注的失败。 例如数据丢失、磁盘空间不足。                                                          |

## 18.4 如何使用

在 `.NET 5` 框架中，微软已经为我们内置了 `日志组件`，正常情况下，无需我们引用第三方包进行日志记录。`.NET 5` 框架为我们提供了两种日志对象创建方式。

### 18.4.1 `ILogger<T>` 泛型方式

使用非常简单，可以通过 `ILogger<T>` 对象进行注入，如：

```cs {5}
public class PrivacyModel : PageModel
{
    private readonly ILogger<PrivacyModel> _logger;

    public PrivacyModel(ILogger<PrivacyModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        _logger.LogInformation("GET Pages.PrivacyModel called.");
    }
}
```

:::tip 小知识

通过泛型 `ILogger<T>` 方式写入日志，那么默认将 `T` 类型完整类型名称作为 `日志类别`。

:::

### 18.4.2 `ILoggerFactory` 工厂方式

使用工厂方式，需手动传入 `日志类别`，如：

```cs {5,7}
public class ContactModel : PageModel
{
    private readonly ILogger _logger;

    public ContactModel(ILoggerFactory logger)
    {
        _logger = logger.CreateLogger("MyCategory");
    }

    public void OnGet()
    {
        _logger.LogInformation("GET Pages.ContactModel called.");
    }
}
```

### 18.4.3 `懒人模式` 😁

在 `Furion` 框架中，提供了更懒的方式写入日志，也就是通过字符串拓展的方式写入，如：

```cs
"简单日志".LogInformation();

"百小僧 新增了一条记录".LogInformation<HomeController>();

"程序出现异常啦".LogError<HomeController>();

"这是自定义类别日志".SetCategory("类别").LogInformation();
```

通过字符串拓展方式可以在任何时候方便记录日志，专门为懒人提供的。

## 18.5 写入其他介质

`.NET 5` 框架中并未提供写入`文件、数据库` 或其他介质的提供器，默认只提供了 `Debug、Console` 两种方式。这个时候我们就需要引用第三方日志组件，方便我们写入到多个介质中。

在这里，`Furion` 官方推荐使用 `Serilog` 日志组件，为此，`Furion` 提供了 `Furion.Extras.Logging.Serilog` 拓展包，方便快速和 `Furion` 框架结合。

### 18.5.1 `Serilog` 拓展包使用

- 安装 `Furion.Extras.Logging.Serilog` 拓展包

在 `Furion.Core` 层安装 `Furion.Extras.Logging.Serilog` 拓展包

- 在 `Program.cs` 中调用 `UseSerilogDefault()`

```cs {21}
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace Furion.Web.Entry
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            return Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.Inject()
                        .UseStartup<Startup>();
                })
                .UseSerilogDefault();
        }
    }
}
```

:::important 特别注意

`.UseSerilogDefault()` 默认集成了 `控制台` 和 `文件` 方式。如需自定义写入，则传入需要写入的介质即可：

```cs
.UseSerilogDefault(config =>
{
    config.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
          .WriteTo.File("log.log", rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true);
});
```

:::

:::important 拓展：可按日志级别 单独输出

```cs
.UseSerilogDefault(config =>//默认集成了 控制台 和 文件 方式。如需自定义写入，则传入需要写入的介质即可：
 {
     string date = DateTime.Now.ToString("yyyy-MM-dd");//按时间创建文件夹
     string outputTemplate = "{NewLine}【{Level:u3}】{Timestamp:yyyy-MM-dd HH:mm:ss.fff}" +
     "{NewLine}#Msg#{Message:lj}" +
     "{NewLine}#Pro #{Properties:j}" +
     "{NewLine}#Exc#{Exception}" +
     new string('-', 50);//输出模板

     ///1.输出所有restrictedToMinimumLevel：LogEventLevel类型
     config
         //.MinimumLevel.Debug() // 所有Sink的最小记录级别
         //.MinimumLevel.Override("Microsoft", LogEventLevel.Fatal)
         //.Enrich.FromLogContext()
         .WriteTo.Console(outputTemplate: outputTemplate)
         .WriteTo.File($"_log/{date}/application.log",
                outputTemplate: outputTemplate,
                 restrictedToMinimumLevel: LogEventLevel.Information,
                 rollingInterval: RollingInterval.Day,//日志按日保存，这样会在文件名称后自动加上日期后缀
                 //rollOnFileSizeLimit: true,          // 限制单个文件的最大长度
                 //retainedFileCountLimit: 10,         // 最大保存文件数,等于null时永远保留文件。
                 //fileSizeLimitBytes: 10 * 1024,      // 最大单个文件大小
                 encoding: Encoding.UTF8            // 文件字符编码
             )

     #region 2.按LogEventLevel.输出独立发布/单文件

        ///2.1仅输出 LogEventLevel.Debug 类型
        .WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(evt => evt.Level == LogEventLevel.Debug)//筛选过滤
            .WriteTo.File($"_log/{date}/{LogEventLevel.Debug}.log",
                outputTemplate: outputTemplate,
                rollingInterval: RollingInterval.Day,//日志按日保存，这样会在文件名称后自动加上日期后缀
                encoding: Encoding.UTF8            // 文件字符编码
             )
         )

        ///2.2仅输出 LogEventLevel.Error 类型
        .WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(evt => evt.Level == LogEventLevel.Error)//筛选过滤
            .WriteTo.File($"_log/{date}/{LogEventLevel.Error}.log",
                outputTemplate: outputTemplate,
                rollingInterval: RollingInterval.Day,//日志按日保存，这样会在文件名称后自动加上日期后缀
                encoding: Encoding.UTF8            // 文件字符编码
             )
         )

     #endregion 按LogEventLevel 独立发布/单文件

         ;
 });
```

:::

- 替换 `appsetting.json` 默认日志内容

```json
"Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore": "Information"
    }
  }
```

替换为：

```json
"Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "System": "Warning",
        "Microsoft": "Warning",
        "Microsoft.Hosting.Lifetime": "Information",
        "Microsoft.EntityFrameworkCore": "Information"
      }
    }
  }
```

### 18.5.2 记录请求日志

`Serilog` 日志组件也提供了非常方便快捷的请求日志中间件，只需要在 `Startup.cs` 中启用即可。如：

```cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
 {
     app.UseStaticFiles();
     app.UseSerilogRequestLogging();    // 必须在 UseStaticFiles 和 UseRouting 之间
     app.UseRouting();
 }
```

## 18.6 日志示例

下面便是日志输出日志的模板，支持各种自定义方式

```
2020-12-21 15:54:43.775 +08:00 [INF] Application started. Press Ctrl+C to shut down.
2020-12-21 15:54:43.897 +08:00 [INF] Hosting environment: Development
2020-12-21 15:54:43.899 +08:00 [INF] Content root path: D:\MONK\Furion\samples\Furion.Web.Entry
2020-12-21 15:55:00.651 +08:00 [WRN] Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data; this mode should only be enabled during development.
2020-12-21 15:55:00.817 +08:00 [INF] Entity Framework Core 5.0.1 initialized 'DefaultDbContext' using provider 'Microsoft.EntityFrameworkCore.Sqlite' with options: SensitiveDataLoggingEnabled DetailedErrorsEnabled MaxPoolSize=100 MigrationsAssembly=Furion.Database.Migrations
2020-12-21 15:55:01.711 +08:00 [WRN] Compiling a query which loads related collections for more than one collection navigation either via 'Include' or through projection but no 'QuerySplittingBehavior' has been configured. By default Entity Framework will use 'QuerySplittingBehavior.SingleQuery' which can potentially result in slow query performance. See https://go.microsoft.com/fwlink/?linkid=2134277 for more information. To identify the query that's triggering this warning call 'ConfigureWarnings(w => w.Throw(RelationalEventId.MultipleCollectionIncludeWarning))'
2020-12-21 15:55:01.919 +08:00 [INF] Executed DbCommand (31ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
SELECT "p"."Id", "p"."Name", "p"."Age", "p"."Address", "p0"."PhoneNumber", "p0"."QQ", "p"."CreatedTime", "p0"."Id", "c"."Id", "c"."Name", "c"."Gender", "t"."Id", "t"."Name", "t"."PersonsId", "t"."PostsId"
FROM "Person" AS "p"
LEFT JOIN "PersonDetail" AS "p0" ON "p"."Id" = "p0"."PersonId"
LEFT JOIN "Children" AS "c" ON "p"."Id" = "c"."PersonId"
LEFT JOIN (
    SELECT "p2"."Id", "p2"."Name", "p1"."PersonsId", "p1"."PostsId"
    FROM "PersonPost" AS "p1"
    INNER JOIN "Post" AS "p2" ON "p1"."PostsId" = "p2"."Id"
) AS "t" ON "p"."Id" = "t"."PersonsId"
ORDER BY "p"."Id", "p0"."Id", "c"."Id", "t"."PersonsId", "t"."PostsId", "t"."Id"
2020-12-21 15:55:25.354 +08:00 [INF] Executed DbCommand (3ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
SELECT "p"."Id", "p"."Name", "p"."Age", "p"."Address", "p0"."PhoneNumber", "p0"."QQ", "p"."CreatedTime", "p0"."Id", "c"."Id", "c"."Name", "c"."Gender", "t"."Id", "t"."Name", "t"."PersonsId", "t"."PostsId"
FROM "Person" AS "p"
LEFT JOIN "PersonDetail" AS "p0" ON "p"."Id" = "p0"."PersonId"
LEFT JOIN "Children" AS "c" ON "p"."Id" = "c"."PersonId"
LEFT JOIN (
    SELECT "p2"."Id", "p2"."Name", "p1"."PersonsId", "p1"."PostsId"
    FROM "PersonPost" AS "p1"
    INNER JOIN "Post" AS "p2" ON "p1"."PostsId" = "p2"."Id"
) AS "t" ON "p"."Id" = "t"."PersonsId"
ORDER BY "p"."Id", "p0"."Id", "c"."Id", "t"."PersonsId", "t"."PostsId", "t"."Id"
2020-12-21 15:58:27.328 +08:00 [INF] Application started. Press Ctrl+C to shut down.
2020-12-21 15:58:27.442 +08:00 [INF] Hosting environment: Development
2020-12-21 15:58:27.444 +08:00 [INF] Content root path: D:\MONK\Furion\samples\Furion.Web.Entry
2020-12-21 15:58:27.909 +08:00 [INF] HTTP GET / responded 200 in 457.0657 ms
2020-12-21 15:58:33.336 +08:00 [INF] HTTP GET /api/index.html responded 200 in 95.9277 ms
2020-12-21 15:58:34.187 +08:00 [INF] HTTP GET /swagger/Default/swagger.json responded 200 in 674.9800 ms
```

## 18.7 打印日志到 `Swagger` 中

在 `Furion` 框架中默认集成了 `MiniProfiler` 组件并与 `Swagger` 进行了结合，如需打印日志或调试代码，只需调用以下方法即可：

```cs
App.PrintToMiniProfiler("分类", "状态", "要打印的消息");
```

## 18.8 在后台任务中使用

由于 `DbContext` 默认注册为 `Scoped` 生存周期，所以在后台任务中使用 `IServiceScopeFactory` 获取所有服务，如：

```cs
public class JobService : BackgroundService
{
    // 日志对象
    private readonly ILogger<JobService> _logger;

    // 服务工厂
    private readonly IServiceScopeFactory _scopeFactory;
    public JobService(ILogger<JobService> logger
        , IServiceScopeFactory scopeFactory)
    {
        _logger = logger;
        _scopeFactory = scopeFactory;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("写日志~~");

        using (var scope = _scopeFactory.CreateScope())
        {
            var services = scope.ServiceProvider;

            // 获取数据库上下文
            var dbContext = Db.GetDbContext(services);

            // 获取仓储
            var respository = Db.GetRepository<Person>(services);

            // 解析其他服务
            var otherService = services.GetService<XXX>();
        }

        return Task.CompletedTask;
    }
}
```

## 18.9 关于日志文件重复生成问题

日志重复生成的原因是创建了多个 `ILogger` 对象导致的，通常有两种解决方法：

1. 设置 `字符串` 拓展日志作用域

```cs
"日志内容".SetLoggerScoped(serviceProvider).LogInformation();
```

2. 修改 `Program.cs` 注册方式

```cs {19}
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace FurStart
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                    .UseStartup<Startup>()
                    .UseSerilogDefault();   // 写到这里
                })
             //   .UseSerilogDefault(); // 注释
    }
}
```

## 18.10 多线程共享作用域

默认情况下，所有的 `字符串` 和 `实体` 拓展都有自己独立维护的 `ServiceProvider` 作用域。

在 `Web` 请求中，默认是 `HttpContext.RequestServices`，但在 `非 Web`，如多线程操作，后台任务，事件总线等场景下会自动创建新的作用域，实际上这是非常不必要的内存开销。

这时，我们只需要通过 `.SetXXXScoped(service)` 共享当前服务提供器作用域即可，如：

```cs
Scoped.Create(async (fac, scope) => {
   "写日志".SetLoggerScoped(scope.ServiceProvider).LogInformation();
});
```

## 18.11 反馈与建议

:::note 与我们交流

给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。

:::

---

:::note 了解更多

想了解更多 `日志` 知识可查阅 [ASP.NET Core - 日志](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0) 章节 和 [Serilog](https://serilog.net/) 文档。

:::
